/*
 * Copyright © 2004-2014 chenYuan. All rights reserved.
 * @Website:wwww.jspx.net
 * @Mail:39793751@qq.com
 * @author: chenYuan , 陈原
 * @License: Jspx.net Framework Code is open source (LGPL)，Jspx.net Framework 使用LGPL 开源授权协议发布。
 * @jvm:jdk1.6+  x86/amd64
 *
 */
package com.jspx.txweb.online.impl;


import com.jspx.boot.sign.*;
import com.jspx.cache.Cache;
import com.jspx.cache.CacheManager;
import com.jspx.cache.JSCacheManager;
import com.jspx.json.JSONObject;
import com.jspx.security.symmetry.Encrypt;
import com.jspx.security.utils.EncryptUtil;
import com.jspx.txweb.Action;
import com.jspx.txweb.IUserSession;
import com.jspx.txweb.bundle.provider.PropertyProvider;
import com.jspx.txweb.dao.MemberDAO;
import com.jspx.txweb.env.TXWeb;
import com.jspx.txweb.env.TXWebIoc;
import com.jspx.txweb.support.ActionSupport;
import com.jspx.txweb.table.*;
import com.jspx.txweb.util.TXWebUtil;
import com.jspx.utils.*;
import com.jspx.txweb.util.RequestUtil;
import com.jspx.txweb.online.OnlineManager;
import com.jspx.boot.environment.Environment;
import com.jspx.boot.EnvFactory;
import com.jspx.boot.environment.EnvironmentTemplate;
import com.jspx.sioc.annotation.Ref;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Cookie;
import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User:chenYuan (mail:39793751@qq.com)
 * Date: 2006-3-8
 * Time: 17:11:07
 * <p/>
 * 规则说明，登录后在将sessionId保存到 session里边，优先使用session中的变量 sessionId
 * 如果没有就检查请求中是否有参数,说明(上传的时候用到),
 * 最后检查cookie中的seesionId(要加密),满足跨域要求,从而实现单点登录
 */
public class OnlineManagerImpl implements OnlineManager {
    final private static Logger log = LoggerFactory.getLogger(OnlineManagerImpl.class);
    final private static int defaultCookieSecond = 60*60*24;
    static private EnvironmentTemplate envTemplate = EnvFactory.getEnvironmentTemplate();
    //上一次清空超时时间，清空超时是全局性的，放这里比较合适
    static private long currentTimeMillisLast = System.currentTimeMillis();
    static private long currentTimeMillisEvict = System.currentTimeMillis(); //进入系统计数，配合刷新使用
    private Encrypt encrypt = EnvFactory.getSymmetryEncrypt();
    //domain 设置你的域名不要www开始为'.',不设置将自动
    private String domain = StringUtil.empty;
    private static boolean fixedSession = true;
    private static int loginTimes = 0;
    private static int overtimeSecond = 10;
    //单点登录,一处登录，另外一处就下线
    private boolean sso = false;
    //允许哪些服务器采用sessionId直接注入登陆
    //配置格式  "(.*).gzcom.gov.cn|(.*).testaio.com"   判断成立的才允许载入
    private String allowServerName = "*";



    public OnlineManagerImpl() {

    }

    public Encrypt getEncrypt()
    {
        return encrypt;
    }


    public void setAllowServerName(String allowServerName)
    {
        this.allowServerName = allowServerName;
    }

    /**
     * 在使用分布式环境，部分服务器会在，session部分分加入分布式服务器ID标识,在设立将设置是否过滤标识
     * 标识为 . 后的字符
     *
     * @param fixedSession 是否修复session
     */
    public void setFixedSession(boolean fixedSession) {
        OnlineManagerImpl.fixedSession = fixedSession;
    }

    public boolean isSso() {
        return sso;
    }

    public void setSso(boolean sso) {
        this.sso = sso;
    }

    /**
     * @param member 用户
     * @return 创建登录日志
     */
    static private LoginLog createLoginLog(Member member) {
        LoginLog loginLog = new LoginLog();
        loginLog.setPutUid(member.getId());
        loginLog.setPutName(member.getName());
        loginLog.setIp(member.getIp());
        return loginLog;
    }

    /**
     * 修复tomcat分布式 session 多余符号
     * @param sessionId
     * @return
     */
    public static String getFixedSessionId(String sessionId) {
        if (fixedSession && sessionId != null && sessionId.contains(".")) {
            return StringUtil.substringBeforeLast(sessionId, ".");
        }
        return sessionId;
    }

    public String getDomain() {
        return domain;
    }

    /**
     * @param domain domain 设置你的域名不要www开始为'.',不设置将自动
     */
    public void setDomain(String domain) {
        this.domain = domain;
    }

    private MemberDAO memberDAO;
    @Ref(name = TXWebIoc.memberDAO, namespace = TXWebIoc.userNamespace)
    public void setMemberDAO(MemberDAO memberDAO) {
        if (this.memberDAO == null)
            this.memberDAO = memberDAO;
    }

    public void init() throws Exception {
        CacheManager cacheManager = JSCacheManager.getCacheManager();
        Cache cache = cacheManager.getCache(Environment.userSessionCache);
        overtimeSecond = cache.getSecond();
        if (overtimeSecond < 10) overtimeSecond = 60;
        memberDAO.deleteOvertimeSession(DateUtil.SECOND * overtimeSecond);
    }

    public boolean isOnline(long uid) {
        return uid > 0 && memberDAO.isOnline(uid);
    }

    public boolean isOnline(String sessionId) {
        sessionId = getFixedSessionId(sessionId);
        return sessionId != null && memberDAO.isOnline(sessionId);
    }

    public Member getGuest()  {
        ///////////////////判断是否有游客帐号，没有就创建一个
        Member member = new Member();
        member.setId(envTemplate.getInt(Environment.guestId, 0));
        member.setName(envTemplate.getString(Environment.guestName, "游客"));
        member.setIp("127.0.0.1");
        member.setPassword(TXWebUtil.createPasswordSaveFormat(RandomUtil.getRandomAlphanumeric(8)));
        member.setPutName(Environment.guestName);
        member.setPutUid(envTemplate.getInt(Environment.guestId));
        ///////////////////
        return member;
    }

    /**
     * @param uid 用户ID
     * @return 如果上次登录，和本次IP不同就提示
     */
    public Map<String, String> getSafePrompt(long uid) {
        Map<String, String> safeInfo = new HashMap<String, String>();
        if (uid<=0) return safeInfo;
        if (memberDAO.getIpPrompt(uid)) {
            safeInfo.put(Environment.promptInfo, "上次登录的IP不一致,ip location check  login safe");
        }
        return safeInfo;
    }



    public static String getLoginType(String loginId)
    {
        String isId  = StringUtil.isMail(loginId)?"mail":null;
        if (StringUtil.isNULL(isId))
            isId = StringUtil.isMobile(loginId)?"phone":"";
        else if (StringUtil.isNULL(isId))
            isId = "name";
        else if (StringUtil.isNumber(isId))
            isId = "uid";
        return isId;
    }
    /**
     * 应用快捷登录
     * @param loginId         登录ID
     * @param password        密码
     * @param passwordEncoded 是否加密,  passwordEncoded:true 表示明文密码
     * @param client          客户端类型
     * @return 提供给应用程序方式登录
     * @throws Exception  异常
     */
    public JSONObject login(HttpSession session,String loginId, String password,int passwordEncoded,String client, String ip) throws Exception {
        JSONObject resultInfo = new JSONObject();
        resultInfo.put(Environment.SUCCESS,YesNoType.NO);
        if ((StringUtil.isNULL(loginId) || StringUtil.getLength(loginId) < 4)) {
            resultInfo.put(Environment.message, "非法的用户名长度,error login name length");
            return resultInfo;
        }

        if (StringUtil.isNULL(password) || password.length() < 6) {
            resultInfo.put(Environment.message,"密码不能少于6个字符,error password");
            return resultInfo;
        }

        if (StringUtil.isNULL(client)) {
            resultInfo.put(Environment.message,"必须说明客户端类型");
            return resultInfo;
        }

        Member member = memberDAO.getMember(getLoginType(loginId), loginId);
        if (member == null) {
            resultInfo.put(Environment.message,"不存在的用户，not found user");
            memberDAO.evict(Member.class);
            return resultInfo;
        }
        if (passwordEncoded==0) password = TXWebUtil.getPasswordHashEncode(password);
        if (!member.equalsPassword(password)) {
            resultInfo.put(Environment.message,"错误的登录ID或密码");
            return resultInfo;
        }

        //判断冻结 begin
        if (member.getCongealType() == CongealType.YES_CONGEAL)
        {
            resultInfo.put(Environment.message,"账号目前已经被冻结, user is congeal don't login");
            //邮件激活
            if (StringUtil.toBoolean(envTemplate.getString(Environment.mailActive))) {
                resultInfo.put(Environment.message,"还没有使用邮件激活, need mail to active");
            }
            return resultInfo;
        }

        if (resultInfo.has(Environment.message)) return resultInfo;
        //信息检查完成


        String sessionId = null;
        if (session!=null)
        {
            sessionId = getFixedSessionId(session.getId());
        } else
            sessionId = getFixedSessionId(EncryptUtil.getHashEncode(member.getId() + member.getName() + client + EnvFactory.getHashAlgorithmKey(),EnvFactory.getHashAlgorithm()));

        //用户session信息更新 begin
        UserSession userSession = new UserSession();
        if (client.contains(StringUtil.COLON))
            userSession.setSystem(StringUtil.substringBefore(client,StringUtil.COLON));
        else
            userSession.setSystem(client);

        userSession.setBrowser(Environment.unknown);
        userSession.setUrl(Environment.unknown);
        userSession.setSid(sessionId);
        userSession.setInvisible(0);
        userSession.setIp(ip);
        //用户session信息更新 end

        //更新用户信息 begin
        member.setLoginTimes(member.getLoginTimes() + 1);
        member.setLoginDate(new Date());
        member.setIp(ip);
        //更新用户信息 end
        userSession.setMember(member);

        if (!StringUtil.isNULL(sessionId)) {
            //SSO  单点登录
            if (sso)
                memberDAO.deleteSession(userSession.getSid(), userSession.getUid());
            memberDAO.save(userSession);
        }
        memberDAO.update(member, new String[]{"loginTimes", "loginDate", "ip"});


        //保存登录日志 begin
        LoginLog loginLog = createLoginLog(member);
        if (client.contains(StringUtil.COLON))
        {
            loginLog.setAppId(StringUtil.toLong(StringUtil.substringAfter(client,StringUtil.COLON)));
        }
        loginLog.setClient(client);
        loginLog.setLoginTimes(member.getLoginTimes());
        loginLog.setSessionId(sessionId);
        loginLog.setIp(ip);
        memberDAO.save(loginLog);
        //保存登录日志 end

        loginTimes++;
        memberDAO.evict(UserSession.class);
        //更新用户信息 end

        if (session!=null)
        {
            session.setAttribute(ActionSupport.Key_UserSession,userSession);
            session.setMaxInactiveInterval(DateUtil.HOUR * 24);
            session.setAttribute(TXWeb.sessionId, userSession.getSid());
        }
        resultInfo.put(Environment.SUCCESS, YesNoType.YES);
        resultInfo.put(TXWeb.sessionId, sessionId);
        return resultInfo;
    }

    /**
     * 判断是否可以登录
     *
     * @param loginId         登录id
     * @param password        密码
     * @param passwordEncoded ture 已经加密
     * @return List<String>
     */
    public Map<String, String> login(ActionSupport action,String isId, String loginId, String password, int passwordEncoded, int cookieSecond, int invisible) throws Exception
    {
        String lan = RequestUtil.getLanguage(action.getRequest());
        PropertyProvider language = new PropertyProvider();
        language.setNamespace(Environment.language);
        language.setDataType(lan);
        language.loadMap();


        Map<String, String> errorInfo = new HashMap<String, String>();
        if (StringUtil.getLength(loginId) < 3) {
            errorInfo.put(Environment.warningInfo, language.getLang(LanguageRes.errorLoginName));
            return errorInfo;
        }
        //tomcat6 bug 修复, 这里如果是中文，很可能接收到的为 编码后的字符串，需要判断解码
        if (loginId.length() > 5 && loginId.startsWith("%") && StringUtil.countMatches(loginId, "%") > 6) {
            loginId = URLUtil.getURLDecoder(loginId, Environment.defaultEncode);
        }
        if (StringUtil.isNULL(password) || password.length() < 4) {
            errorInfo.put(Environment.warningInfo, language.getLang(LanguageRes.errorPassword));
            return errorInfo;
        }
        final HttpSession session = action.getSession();
        Member member = null;
        //短信方式登录
        if (LoginField.Sms.equalsIgnoreCase(isId))
        {
            member = memberDAO.getMember(LoginField.Phone, loginId);
            if (member == null) {
                errorInfo.put(Environment.warningInfo,language.getLang(LanguageRes.noFoundUser));
                memberDAO.evict(Member.class);
                return errorInfo;
            }
            String smsValidate = (String)session.getAttribute(TXWeb.jspxSmsValidate);
            if (!password.equalsIgnoreCase(smsValidate)) {
                errorInfo.put(Environment.warningInfo, language.getLang(LanguageRes.errorSmsValid));
                return errorInfo;
            }
        } else {
            if (StringUtil.isNULL(isId)) isId = getLoginType(loginId);
            member = memberDAO.getMember(isId, loginId);
            if (member == null) {
                errorInfo.put(Environment.warningInfo, language.getLang(LanguageRes.noFoundUser));
                memberDAO.evict(Member.class);
                return errorInfo;
            }
            //passwordEncoded 0:表示默认密码方式
            if (passwordEncoded==0) password = TXWebUtil.getPasswordHashEncode(password);

            if (!member.equalsPassword(password)) {
                errorInfo.put(Environment.warningInfo, language.getLang(LanguageRes.errorNameOrPassword));
                return errorInfo;
            }
        }

        //判断冻结 begin
        if (member.getCongealType() == CongealType.YES_CONGEAL)
        {
            errorInfo.put(Environment.warningInfo, language.getLang(LanguageRes.errorNameOrPassword));
            //邮件激活
            if (StringUtil.toBoolean(envTemplate.getString(Environment.mailActive))) {
                errorInfo.put(Environment.warningInfo, language.getLang(LanguageRes.needMailActive));
            }
            return errorInfo;
        }

        //信息检查完成
        final HttpServletRequest request = action.getRequest();
        UserSession userSession = (UserSession)action.getUserSession();
        //用户session信息更新 begin
        if (userSession==null) userSession = new UserSession();
        if (StringUtil.isNULL(userSession.getSid()))
        {
            String sessionId = getFixedSessionId(session.getId());
            userSession.setSid(sessionId);
        }
        userSession.setMember(member);
        userSession.setUrl(request.getRequestURL().toString());
        userSession.setSystem(RequestUtil.getSystem(request));
        userSession.setBrowser(RequestUtil.getBrowser(request));
        userSession.setIp(RequestUtil.getRemoteAddr(request));
        userSession.setInvisible(invisible);
        //用户session信息更新 end

        if (cookieSecond<=0) cookieSecond = defaultCookieSecond;
        session.setMaxInactiveInterval(cookieSecond * DateUtil.SECOND);
        session.setAttribute(TXWeb.sessionId, userSession.getSid());

        //单点登录，就删除其他的登录信息
        if (sso&&userSession.getUid()>0) deleteUserSession(userSession.getSid(), userSession.getUid());

        if (sso||userSession.getId()<=0)
        {
            userSession.setId(0);
            memberDAO.save(userSession);
        } else
        {
            memberDAO.update(userSession);
        }
        //如果有必要，这里加入同步 end

        //用户信息更新 begin
        member.setLoginTimes(member.getLoginTimes() + 1);
        member.setLoginDate(new Date());
        member.setIp(RequestUtil.getRemoteAddr(request));
        memberDAO.update(member, new String[]{"loginTimes", "loginDate", "ip"});
        //更新用户信息 end

        //保存登录日志 begin
        LoginLog loginLog = createLoginLog(member);
        loginLog.setClient("web");
        loginLog.setLoginTimes(member.getLoginTimes());
        loginLog.setSessionId(userSession.getSid());
        memberDAO.save(loginLog);
        //保存登录日志 end

        //登录的时候作为验证
        memberDAO.evict(UserSession.class);
        loginTimes++;

        setCookieTicket(request, action.getResponse(), userSession.getSid(), cookieSecond);
        action.put(ActionSupport.Key_UserSession,userSession);
        session.setAttribute(ActionSupport.Key_UserSession,userSession);
        return errorInfo;
    }

    public void exit(ActionSupport action) throws Exception
    {
        HttpServletResponse response = action.getResponse();
        if (response!=null)
        {
            CookieUtil.cookieClear(action.getRequest(),response);
        }
        IUserSession userSession = (IUserSession)action.getUserSession();
        if (userSession!=null)
            exit(userSession.getSid());
        SessionUtil.cleanAll(action.getRequest());
    }

    public void exit(String sessionId) throws Exception
    {
        sessionId = getFixedSessionId(sessionId);
        memberDAO.deleteSession(sessionId,0);
        memberDAO.evict(UserSession.class);
    }

    /**
     * @param sessionId 绘画id
     * @return 单点登录
     * @throws Exception 异常
     */
    public UserSession getUserSession(String sessionId) throws Exception {
        sessionId = getFixedSessionId(sessionId);
        UserSession userSession = memberDAO.getUserSession(sessionId);
        if (StringUtil.isNULL(userSession.getSid()))
        {
            userSession.setSystem(Environment.unknown);
            userSession.setBrowser(Environment.unknown);
            userSession.setUrl(Environment.unknown);
            userSession.setIp("127.0.0.1");
            userSession.setSid(sessionId);
            userSession.setUrl(Environment.none);
            userSession.setMember(getGuest());
            //创建一个游客
            if (!StringUtil.isNULL(userSession.getSid())) {
                memberDAO.save(userSession);
            }
            return userSession;
        } else if (System.currentTimeMillis() - userSession.getLastRequestTime() > 3 * DateUtil.MINUTE) {
            userSession.setLastRequestTime(System.currentTimeMillis());
            if (memberDAO.update(userSession, new String[]{"lastRequestTime"}) > 0)
                deleteOvertime();
        }
        return userSession;
    }

    /**
     * 得到在线用户信息,并且可以自动登录
     *
     * @param action 页面对象
     * @return UserSession 用户session
     * <p/>
     * 规则说明，登录后在将sessionId保存到 session里边，优先使用session中的变量 sessionId
     * 如果没有就检查请求中是否有参数,说明(上传的时候用到),
     * 最后检查cookie中的seesionId(要加密),满足跨域要求,从而实现单点登录
     */
    public UserSession getUserSession(Action action) throws Exception {
        //有可能多个拦截器，之前已经读取过一次了
        if (action.getEnv().containsKey(ActionSupport.Key_UserSession)) {
            UserSession userSession = (UserSession) action.getUserSession();
            if (userSession!=null&&!userSession.isGuest()) return userSession;
        }

        //DDOS攻击检查 begin
        //DDOS 攻击 sessionId 不同
        String sessionId = action.getString(TXWeb.sessionId);
        final HttpServletRequest request = action.getRequest();

        //外挂接收登陆信息有一定风险性
        if (!StringUtil.isNULL(sessionId) && allowServerName != null && !"*".equals(allowServerName) && request != null) {
            if (!request.getServerName().matches(allowServerName)) sessionId = null;
        }
        if (StringUtil.isNULL(sessionId) || sessionId.length() > 100) {
            sessionId = action.getEnv(TXWeb.sessionId);
        }

        final HttpSession session = action.getSession();
        if (StringUtil.isNULL(sessionId) || sessionId.length() > 100) {
            sessionId  = (String)session.getAttribute(TXWeb.sessionId);
        }

        if (StringUtil.isNULL(sessionId) || sessionId.length() > 100) {
            sessionId = getCookieSessionId(request,action.getResponse());
        }
        //如果是二级域名共享登陆

        if (session != null && StringUtil.isNULL(sessionId))
        {
            sessionId = session.getId();
        }
        //远程方式登录监测 begin
        sessionId = getFixedSessionId(sessionId);
        //远程方式登录监测 end

        UserSession userSession = null;
        if (StringUtil.hasLength(sessionId))
        {
            userSession = memberDAO.getUserSession(sessionId);
        }
        if (userSession!=null&&StringUtil.hasLength(userSession.getSid()))
        {
            if (session!=null&&!sessionId.equals(session.getAttribute(TXWeb.sessionId))) session.setAttribute(TXWeb.sessionId,userSession.getSid());
        }
        else {
            synchronized (this)
            {
                userSession = new UserSession();
                userSession.setUrl(request.getRequestURL().toString());
                userSession.setSystem(RequestUtil.getSystem(request));
                userSession.setBrowser(RequestUtil.getBrowser(request));
                userSession.setIp(RequestUtil.getRemoteAddr(request));
                userSession.setSid(sessionId);
                if (session!=null) session.setAttribute(TXWeb.sessionId,sessionId);
                memberDAO.save(userSession);
            }
            return userSession;
        }

       if (request != null && System.currentTimeMillis() - userSession.getLastRequestTime() > 3 * DateUtil.MINUTE) {
            userSession.setUrl(request.getRequestURL().toString());
            userSession.setSystem(RequestUtil.getSystem(request));
            userSession.setBrowser(RequestUtil.getBrowser(request));
            userSession.setNetType(RequestUtil.getNetType(request));
            userSession.setIp(RequestUtil.getRemoteAddr(request));
            userSession.setLastRequestTime(System.currentTimeMillis());
            memberDAO.update(userSession, new String[]{"url", "system", "browser", "ip", "lastRequestTime", "netType"});
        }

        //最小更新时间为10秒，避免进入的太多，不断清空
        if (loginTimes > 0 && System.currentTimeMillis() - currentTimeMillisEvict > overtimeSecond) {
            memberDAO.evict(UserSession.class);
            currentTimeMillisEvict = System.currentTimeMillis();
            loginTimes = 0;
        } else
            deleteOvertime();
        return userSession;
    }
    /**
     * @param sessionId 用户sessionID
     * @param uid       用户ID
     * @return 删除是否成功
     * @throws Exception 异常
     */
    public void deleteUserSession(String sessionId, long uid) throws Exception {
        sessionId = getFixedSessionId(sessionId);
        memberDAO.deleteSession(sessionId, uid);
    }

    /**
     * @param request      请求
     * @param response     应答
     * @param sid          sessionID
     * @param cookieSecond 保存时间
     * @throws Exception 异常
     */
    public void setCookieTicket(HttpServletRequest request, HttpServletResponse response, String sid, int cookieSecond) {
        //保存 Cookie begin
        if (StringUtil.isNULL(sid)) return;
        String sessionEncodeKey = null;
        try {
            sessionEncodeKey = encrypt.getEncode(sid);
        } catch (Exception e) {
            log.debug("setCookieTicket encrypt encode fail",e);
        }
        Cookie sessionCookie = new Cookie(TXWeb.COOKIE_TICKET, sessionEncodeKey);
        sessionCookie.setMaxAge(cookieSecond * DateUtil.SECOND);
        sessionCookie.setPath("/");
        sessionCookie.setHttpOnly(true);
        sessionCookie.setSecure(true);
        if (!StringUtil.isNULL(domain)) {
            sessionCookie.setDomain(domain);
        } else if (request != null) {
            sessionCookie.setDomain(URLUtil.getTopDomain(request.getServerName()));
        }
        response.addCookie(sessionCookie);
    }

    /**
     * @param request 请求
     * @return 得到sessionID
     * @throws Exception  异常
     */
    public String getCookieSessionId(HttpServletRequest request,HttpServletResponse response)
    {
        if (request == null) return null;
        //优先单点登录方式
        String cookeValue = CookieUtil.getCookieString(request, TXWeb.COOKIE_TICKET, null);
        if (StringUtil.isNULL(cookeValue)||cookeValue.length()<6 ) return StringUtil.empty;
        try {
            return encrypt.getDecode(cookeValue);
        } catch (Exception e) {
            CookieUtil.cookieClear(request, response);
            log.debug("getCookieSessionId fail",e);
        }
        return StringUtil.empty;
    }

    public void destroy() {
        if (memberDAO != null) memberDAO.evict(UserSession.class);
    }

    private void deleteOvertime() {
        if (System.currentTimeMillis() - currentTimeMillisLast > (DateUtil.SECOND * overtimeSecond)) {
            memberDAO.deleteOvertimeSession(DateUtil.SECOND * overtimeSecond);
            currentTimeMillisLast = System.currentTimeMillis();
        }
    }

}